/*
 * SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
 * SPDX-License-Identifier: GPL-3.0-only
 */

package com.sanji.audio_test.output

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.provider.CallLog
import android.telecom.Call
import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager
import android.util.Log
import androidx.core.database.getStringOrNull
import com.sanji.audio_test.extension.phoneNumber
import com.sanji.audio_test.withContactsByPhoneNumber
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime

/**
 * 通话元数据收集器
 * 用于收集通话相关信息，如参与者、时间、方向等
 *
 * @param context 应用上下文
 * @param parentCall 父通话对象
 */
class CallMetadataCollector(
    private val context: Context,
    private val parentCall: Call,
) {
    // 通话信息映射表
    private val callDetails = mutableMapOf<Call, Call.Details>()
    // 是否为会议通话
    private val isConference = parentCall.details.hasProperty(Call.Details.PROPERTY_CONFERENCE)
    // 通话元数据（延迟初始化）
    private lateinit var _callMetadata: CallMetadata
    // 公开的通话元数据属性，线程安全访问
    val callMetadata: CallMetadata
        get() = synchronized(this) {
            _callMetadata
        }

    /**
     * 初始化块，设置初始通话详情并更新元数据
     */
    init {
        callDetails[parentCall] = parentCall.details
        if (isConference) {
            for (childCall in parentCall.children) {
                callDetails[childCall] = childCall.details
            }
        }

        update(false)
    }

    /**
     * 根据电话号码获取联系人显示名称
     *
     * @param number 电话号码
     * @param allowManualLookup 是否允许手动查找联系人
     * @return 联系人显示名称，如果找不到则返回null
     */
    private fun getContactDisplayNameByNumber(number: PhoneNumber, allowManualLookup: Boolean): String? {
        // 除非是最后一次文件名更新，否则禁用此功能，因为它会同步阻塞
        if (!allowManualLookup) {
            Log.d(TAG, "本次调用禁用手动联系人查找")
            return null
        }

        if (context.checkSelfPermission(Manifest.permission.READ_CONTACTS) !=
            PackageManager.PERMISSION_GRANTED) {
            Log.w(TAG, "未获得查找联系人的权限")
            return null
        }

        Log.d(TAG, "执行手动联系人查找")

        val contact = try {
            withContactsByPhoneNumber(context, number) { it.firstOrNull() }
        } catch (e: Exception) {
            Log.d(TAG, "查找联系人失败", e)
            return null
        }
        if (contact == null) {
            Log.d(TAG, "通过手动查找未找到联系人")
            return null
        }

        Log.d(TAG, "通过手动查找找到联系人显示名称")
        return contact.displayName
    }

    /**
     * 获取联系人显示名称
     *
     * @param details 通话详情
     * @param allowManualLookup 是否允许手动查找联系人
     * @return 联系人显示名称，如果找不到则返回null
     */
    private fun getContactDisplayName(details: Call.Details, allowManualLookup: Boolean): String? {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            val name = details.contactDisplayName
            if (name != null) {
                return name
            }
        }

        // 在会议通话中，电信框架有时不会为每个参与者返回联系人显示名称，所以我们自己查找
        // 这类似于InCallUI的做法，只是它甚至不尝试查看contactDisplayName
        if (isConference) {
            Log.w(TAG, "会议通话子通话中缺少联系人显示名称")
        }

        val number = details.phoneNumber
        if (number == null) {
            Log.w(TAG, "无法从通话中确定电话号码")
            return null
        }

        return getContactDisplayNameByNumber(number, allowManualLookup)
    }

    /**
     * 从通话记录获取详情
     *
     * @param parentDetails 父通话详情
     * @param allowBlockingCalls 是否允许阻塞调用
     * @return 包含电话号码和名称的Pair，如果找不到则对应项为null
     */
    private fun getCallLogDetails(
        parentDetails: Call.Details,
        allowBlockingCalls: Boolean,
    ): Pair<PhoneNumber?, String?> {
        // 除非是最后一次文件名更新，否则禁用此功能，因为它会同步阻塞
        if (!allowBlockingCalls) {
            Log.d(TAG, "本次调用禁用通话记录查找")
            return Pair(null, null)
        }

        if (context.checkSelfPermission(Manifest.permission.READ_CALL_LOG) !=
            PackageManager.PERMISSION_GRANTED) {
            Log.w(TAG, "未获得查找通话记录的权限")
            return Pair(null, null)
        }

        // 通话记录不会显示会议通话的所有参与者
        if (isConference) {
            Log.w(TAG, "由于是会议通话，跳过通话记录查找")
            return Pair(null, null)
        }

        val uri = CallLog.Calls.CONTENT_URI.buildUpon()
            .appendQueryParameter(CallLog.Calls.LIMIT_PARAM_KEY, "1")
            .build()

        // System.nanoTime() 比 Instant.now() 更可能是单调的
        val start = System.nanoTime()
        var attempt = 1

        var number: PhoneNumber? = null
        var name: String? = null

        while (true) {
            val now = System.nanoTime()
            if (now >= start + CALL_LOG_QUERY_TIMEOUT_NANOS) {
                break
            }

            val prefix = "[尝试 #$attempt @ ${(now - start) / 1_000_000}ms] "

            context.contentResolver.query(
                uri,
                arrayOf(CallLog.Calls.CACHED_NAME, CallLog.Calls.NUMBER),
                "${CallLog.Calls.DATE} = ?",
                arrayOf(parentDetails.creationTimeMillis.toString()),
                "${CallLog.Calls._ID} DESC",
            )?.use { cursor ->
                if (cursor.moveToFirst()) {
                    Log.d(TAG, "${prefix}找到通话记录条目")

                    if (number == null) {
                        val index = cursor.getColumnIndex(CallLog.Calls.NUMBER)
                        if (index != -1) {
                            number = cursor.getStringOrNull(index)?.let {
                                Log.d(TAG, "${prefix}找到通话记录电话号码")
                                try {
                                    PhoneNumber(it)
                                } catch (e: IllegalArgumentException) {
                                    Log.w(TAG, "${prefix}无效的通话记录电话号码", e)
                                    null
                                }
                            }
                        } else {
                            Log.d(TAG, "${prefix}通话记录条目没有电话号码")
                        }
                    }

                    if (name == null) {
                        val index = cursor.getColumnIndex(CallLog.Calls.CACHED_NAME)
                        if (index != -1) {
                            name = cursor.getStringOrNull(index)?.let {
                                Log.d(TAG, "${prefix}找到通话记录缓存名称")
                                it
                            }
                        } else {
                            Log.d(TAG, "${prefix}通话记录条目没有缓存名称")
                        }
                    }

                    Unit
                } else {
                    Log.d(TAG, "${prefix}未找到通话记录条目")
                }
            }

            attempt += 1

            if (number != null && name != null) {
                break
            }

            Thread.sleep(CALL_LOG_QUERY_RETRY_DELAY_MILLIS)
        }

        if (number != null && name != null) {
            Log.d(TAG, "经过 ${attempt - 1} 次尝试后找到所有通话记录详情")
        } else {
            Log.d(TAG, "经过所有 ${attempt - 1} 次尝试后通话记录详情不完整")
        }

        return Pair(number, name)
    }

    /**
     * 计算通话元数据
     *
     * @param parentDetails 父通话详情
     * @param displayDetails 显示的通话详情列表
     * @param allowBlockingCalls 是否允许阻塞调用
     * @return 通话元数据
     */
    private fun computeMetadata(
        parentDetails: Call.Details,
        displayDetails: List<Call.Details>,
        allowBlockingCalls: Boolean,
    ): CallMetadata {
        val instant = Instant.ofEpochMilli(parentDetails.creationTimeMillis)
        val timestamp = ZonedDateTime.ofInstant(instant, ZoneId.systemDefault())

        // AOSP的电信框架内部文档指定了会议通话的方向在足够多的参与者挂断之前是无意义的，
        // 直到它变成模拟的一对一通话
        val direction = if (isConference) {
            CallDirection.CONFERENCE
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            when (parentDetails.callDirection) {
                Call.Details.DIRECTION_INCOMING -> CallDirection.IN
                Call.Details.DIRECTION_OUTGOING -> CallDirection.OUT
                Call.Details.DIRECTION_UNKNOWN -> null
                else -> null
            }
        } else {
            null
        }

        var simCount: Int? = null
        var simSlot: Int? = null

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
            && context.checkSelfPermission(Manifest.permission.READ_PHONE_STATE)
                == PackageManager.PERMISSION_GRANTED) {
            val subscriptionManager = context.getSystemService(SubscriptionManager::class.java)

            val telephonyManager = context.getSystemService(TelephonyManager::class.java)
            val subscriptionId = telephonyManager.getSubscriptionId(parentDetails.accountHandle)
            val subscriptionInfo = subscriptionManager.getActiveSubscriptionInfo(subscriptionId)

            simCount = subscriptionManager.activeSubscriptionInfoCount
            simSlot = subscriptionInfo?.let { it.simSlotIndex + 1 }
        }

        val (callLogNumber, callLogName) = getCallLogDetails(parentDetails, allowBlockingCalls)

        var calls = displayDetails.map {
            CallPartyDetails(
                it.phoneNumber,
                it.callerDisplayName,
                getContactDisplayName(it, allowBlockingCalls),
            )
        }

        if (callLogNumber != null && !calls.any { it.phoneNumber == callLogNumber }) {
            Log.w(TAG, "通话记录电话号码与任何通话句柄不匹配")
            Log.w(TAG, "假设通话被转接，信任通话记录")

            calls = listOf(
                CallPartyDetails(
                    callLogNumber,
                    null,
                    getContactDisplayNameByNumber(callLogNumber, allowBlockingCalls),
                )
            )
        }

        return CallMetadata(
            timestamp,
            direction,
            simCount,
            simSlot,
            callLogName,
            calls,
        )
    }

    /**
     * 更新通话元数据
     *
     * @param allowBlockingCalls 是否允许阻塞调用
     * @return 更新后的通话元数据
     */
    fun update(allowBlockingCalls: Boolean): CallMetadata {
        val parentDetails = callDetails[parentCall]!!
        val displayDetails = if (isConference) {
            callDetails.entries.asSequence()
                .filter { it.key != parentCall }
                .map { it.value }
                .toList()
        } else {
            listOf(parentDetails)
        }

        val metadata = computeMetadata(parentDetails, displayDetails, allowBlockingCalls)
        synchronized(this) {
            _callMetadata = metadata
        }

        return metadata
    }

    /**
     * 使用来自[details]的信息更新状态
     *
     * @param call 父通话或其子通话（对于会议通话）
     * @param details 属于[call]的更新后的通话详情
     * @return 更新后的通话元数据
     */
    fun updateCallDetails(call: Call, details: Call.Details): CallMetadata {
        if (call !== parentCall && call.parent !== parentCall) {
            throw IllegalStateException("既不是父通话也不是其子通话: $call")
        }

        synchronized(this) {
            callDetails[call] = details

            return update(false)
        }
    }

    companion object {
        private val TAG = CallMetadataCollector::class.java.simpleName

        // 通话记录查询超时时间（纳秒）
        private const val CALL_LOG_QUERY_TIMEOUT_NANOS = 2_000_000_000L
        // 通话记录查询重试延迟（毫秒）
        private const val CALL_LOG_QUERY_RETRY_DELAY_MILLIS = 100L
    }
}